Skip to content

release: 2.1.0 — JRuby Java escaper, TruffleRuby on pure Ruby, Ruby 4 - #10

Merged
sebyx07 merged 2 commits into
mainfrom
jruby-java-escaper-and-ruby4
Oct 31, 2026
Merged

sebyx07 merged 2 commits into
mainfrom
jruby-java-escaper-and-ruby4

Conversation

@sebyx07

@sebyx07 sebyx07 commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Merging this (a version.rb bump to 2.1.0) publishes both the C-extension gem and the new -java gem via trusted publishing, then tags v2.1.0.

What's in it

JRuby — Java escaper (~1.9× faster)

A tiny Java extension (ext/java, Ruby2html::JavaNative) accelerates HTML escaping — the renderer's hot path — over the pure-Ruby twin. Measured on JRuby 10.1: 13k → 25k renders/s, byte-identical output. Everything else stays pure Ruby. Shipped as a ruby2html-<v>-java platform gem with the jar prebuilt (no JDK needed to install).

TruffleRuby — pure Ruby, not Sulong

TruffleRuby runs C extensions through Sulong but JITs pure Ruby with Graal, which here renders ~28× faster (822 → 23k i/s). extconf.rb/render.rb now prefer pure Ruby there; Ruby2html.native? is false on TruffleRuby.

Ruby 4

.ruby-version → 4.0.9 for the pinned CI jobs (full suite, -Werror gcc/clang, rubocop — all green). The CRuby matrix already tracks the official list (3.3, 3.4, 4.0) automatically.

Release / CI

  • release.yml builds the -java gem on JRuby and publishes both gems.
  • CI engines leg builds + smoke-tests the jar; gem-smoke fails loudly if a -java gem lacks the jar.
  • Platform-aware gemspec that never raises at load.

Verification (local, on real engines)

  • CRuby 4.0.9: 225 examples, 0 failures (native + pure-Ruby mode); rubocop clean.
  • JRuby 10.1: 85 standalone examples, 0 failures; -java gem builds, installs, renders; Java escaper byte-identical across UTF-8/US-ASCII/BINARY/invalid bytes.
  • TruffleRuby 24.2: 84 examples, 0 failures on pure Ruby; smoke passes.
  • Independent XSS/escaping audit: no hole, byte-for-byte and encoding-label identical to the C reference.

Invariant check: the Java escaper is a third side of the escape rule only — kept byte-identical with html_escape.c / PureRuby.escape (a render_spec example asserts it's loaded and matches on JRuby).

🤖 Generated with Claude Code

Faster, correct native paths for the non-CRuby engines, plus a bump to
the current stable Ruby for the pinned CI jobs.

JRuby: add a tiny Java extension (ext/java, Ruby2html::JavaNative) that
accelerates HTML escaping -- the renderer's hot path -- over the
pure-Ruby twin. ~1.9x faster end-to-end rendering (13k -> 25k renders/s),
byte-identical output (verified against the C extension and the regex
branch across UTF-8/US-ASCII/BINARY/invalid bytes). Shipped as a
ruby2html-<version>-java platform gem with the jar prebuilt (no JDK to
install); everything else stays pure Ruby. Built by rake-compiler's
JavaExtensionTask.

TruffleRuby: stop loading the C extension. TruffleRuby runs C through
Sulong but JITs pure Ruby with Graal, which renders ~28x faster here;
extconf.rb and render.rb now prefer pure Ruby, so Ruby2html.native? is
false on TruffleRuby.

Ruby 4: .ruby-version -> 4.0.9 (full suite, strict -Werror build and
rubocop all green). The CRuby test matrix already tracks the official
maintained-versions list (3.3, 3.4, 4.0) with no edit.

Release/CI: release.yml builds the -java gem on JRuby and publishes both
gems; the CI engines leg builds and smoke-tests the jar; gem-smoke fails
loudly if a -java gem lacks the jar; the gemspec is platform-aware and
never raises at load. Docs (AGENTS.md, README, llms.txt, CHANGELOG)
updated; wurk noted as a sibling under the same developerz.ai model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Benchmarks: x86_64 (linux)

ruby 4.0.9 (2026-10-23 revision 3e93b23afc) +PRISM [x86_64-linux] · commit e300bb3ff9 · 2.0s per report
Δ is against main 95a8324209 (same runner type; expect a few % noise).

escape_bench (C ext) · peak RSS 119 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
escape_html short clean (10B) 615,272 ± 0.4% 0.0016 0.00 fastest +3.9%
escape_html medium clean (200B) 378,660 ± 4.4% 0.0026 0.00 1.62x slower +1.0%
append short clean (10B) 237,762 ± 0.4% 0.0042 0.02 2.59x slower +11.4%
append medium clean (200B) 164,979 ± 0.3% 0.0061 0.00 3.73x slower +1.5%
append short dirty (10B) 162,468 ± 0.4% 0.0062 0.00 3.79x slower +10.6%
escape_html short dirty (10B) 122,531 ± 1.1% 0.0082 100 5.02x slower +174.5%
append medium dirty (200B) 91,522 ± 0.3% 0.0109 0.00 6.72x slower -3.2%
escape_html medium dirty (200B) 71,638 ± 5.1% 0.0140 100 8.59x slower +9.7%
escape_html long clean (10KB) 16,102 ± 0.9% 0.0621 0.00 38.21x slower -0.1%
append long clean (10KB) 10,950 ± 1.8% 0.0913 0.00 56.19x slower +1.7%
append long dirty (10KB) 1,591 ± 0.3% 0.6287 0.00 386.82x slower +5.1%
escape_html long dirty (10KB) 1,534 ± 3.9% 0.6519 100 401.12x slower +21.7%

escape_bench (pure Ruby) · peak RSS 145 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
escape_html short clean (10B) 191,980 ± 6.6% 0.0052 0.00 fastest -8.9%
append short clean (10B) 146,834 ± 4.0% 0.0068 0.02 1.31x slower +0.7%
escape_html medium clean (200B) 85,020 ± 1.7% 0.0118 0.00 2.26x slower -2.7%
escape_html short dirty (10B) 84,388 ± 1.3% 0.0118 100 2.27x slower +157.1%
append medium clean (200B) 68,839 ± 1.1% 0.0145 0.00 2.79x slower -4.8%
append short dirty (10B) 65,711 ± 5.7% 0.0152 100 2.92x slower +116.2%
escape_html medium dirty (200B) 28,862 ± 5.0% 0.0346 200 6.65x slower +0.7%
append medium dirty (200B) 26,150 ± 0.9% 0.0382 200 7.34x slower +4.2%
escape_html long clean (10KB) 3,059 ± 0.7% 0.3269 0.00 62.77x slower -0.1%
append long clean (10KB) 2,768 ± 1.0% 0.3613 0.00 69.37x slower -0.3%
escape_html long dirty (10KB) 1,375 ± 0.5% 0.7274 200 139.65x slower +3.2%
append long dirty (10KB) 1,157 ± 4.8% 0.8641 200 165.89x slower +11.4%

render_bench (C ext) · peak RSS 131 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
erubi 8,286 ± 0.9% 0.1207 713 fastest +17.4%
ruby2html 5,284 ± 0.5% 0.1892 419 1.57x slower +0.9%
phlex 507 ± 1.8% 1.9723 6,374 16.34x slower +12.9%

render_bench (pure Ruby) · peak RSS 132 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
erubi 8,298 ± 1.1% 0.1205 713 fastest +65.8%
ruby2html 2,254 ± 1.2% 0.4437 1,029 3.68x slower +15.8%
phlex 520 ± 1.3% 1.9240 6,374 15.96x slower +21.3%

tag_bench (C ext) · peak RSS 40 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
br 255,016 ± 1.2% 0.0039 7.00 fastest +5.4%
li(text) 165,546 ± 3.2% 0.0060 7.02 1.54x slower +2.9%
li(dirty text) 131,468 ± 4.5% 0.0076 7.00 1.94x slower +3.6%
div { block } 116,081 ± 1.1% 0.0086 7.00 2.20x slower -7.3%
span(text, class:) 29,124 ± 0.8% 0.0343 207 8.76x slower +6.4%
div(class:) { block } 26,839 ± 0.3% 0.0373 207 9.50x slower +3.1%
a(text, href:, class:) 21,779 ± 0.4% 0.0459 207 11.71x slower +4.6%

tag_bench (pure Ruby) · peak RSS 26 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
br 97,501 ± 3.4% 0.0103 107 fastest +14.2%
div { block } 80,638 ± 0.4% 0.0124 107 1.21x slower +13.1%
li(text) 70,893 ± 0.3% 0.0141 107 1.38x slower +10.5%
li(dirty text) 50,366 ± 0.4% 0.0199 207 1.94x slower +10.4%
span(text, class:) 15,041 ± 0.6% 0.0665 207 6.48x slower +4.4%
div(class:) { block } 13,525 ± 3.6% 0.0739 207 7.21x slower -0.7%
a(text, href:, class:) 9,410 ± 1.2% 0.1063 207 10.36x slower -3.6%

Rails request (C ext) · peak RSS 244 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
Slim 516 ± 5.0% 1.9377 4,101 fastest -4.2%
Ruby2html::Component 510 ± 2.2% 1.9615 4,427 1.01x slower -2.6%
ruby2html .html.rb templates 496 ± 2.6% 2.0169 4,563 1.04x slower -4.9%
ViewComponent + ruby2html 466 ± 4.5% 2.1456 4,284 1.11x slower -5.5%
Phlex 446 ± 1.1% 2.2440 5,773 1.16x slower -6.2%
ERB 434 ± 18.6% 2.3018 2,924 1.19x slower -12.8%

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Benchmarks: aarch64 (linux)

ruby 4.0.9 (2026-10-23 revision 3e93b23afc) +PRISM [aarch64-linux] · commit e300bb3ff9 · 2.0s per report
Δ is against main 95a8324209 (same runner type; expect a few % noise).

escape_bench (C ext) · peak RSS 118 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
escape_html short clean (10B) 595,165 ± 0.3% 0.0017 0.00 fastest +2.0%
escape_html medium clean (200B) 322,626 ± 0.2% 0.0031 0.00 1.84x slower +0.9%
append short clean (10B) 212,105 ± 0.3% 0.0047 0.02 2.81x slower +14.8%
append short dirty (10B) 161,949 ± 0.5% 0.0062 0.00 3.68x slower +13.0%
append medium clean (200B) 146,567 ± 0.8% 0.0068 0.00 4.06x slower +1.3%
escape_html short dirty (10B) 107,577 ± 1.0% 0.0093 100 5.53x slower +130.8%
append medium dirty (200B) 74,121 ± 0.2% 0.0135 0.00 8.03x slower +1.7%
escape_html medium dirty (200B) 57,712 ± 0.4% 0.0173 100 10.31x slower +6.0%
escape_html long clean (10KB) 11,320 ± 0.2% 0.0883 0.00 52.58x slower +0.0%
append long clean (10KB) 9,000 ± 0.2% 0.1111 0.00 66.13x slower -3.2%
append long dirty (10KB) 1,193 ± 0.8% 0.8386 0.00 499.08x slower +1.8%
escape_html long dirty (10KB) 1,132 ± 3.1% 0.8836 100 525.88x slower +13.9%

escape_bench (pure Ruby) · peak RSS 150 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
escape_html short clean (10B) 164,678 ± 0.2% 0.0061 0.00 fastest +6.9%
append short clean (10B) 104,419 ± 0.3% 0.0096 0.02 1.58x slower -1.5%
escape_html short dirty (10B) 68,292 ± 0.7% 0.0146 100 2.41x slower +125.3%
escape_html medium clean (200B) 56,888 ± 0.2% 0.0176 0.00 2.89x slower +2.5%
append short dirty (10B) 56,881 ± 0.5% 0.0176 100 2.90x slower +102.4%
append medium clean (200B) 45,502 ± 0.2% 0.0220 0.00 3.62x slower -3.9%
escape_html medium dirty (200B) 24,106 ± 0.8% 0.0415 200 6.83x slower +6.5%
append medium dirty (200B) 21,607 ± 0.8% 0.0463 200 7.62x slower +4.0%
escape_html long clean (10KB) 1,631 ± 0.2% 0.6131 0.00 100.96x slower +0.2%
append long clean (10KB) 1,545 ± 0.3% 0.6472 0.00 106.58x slower -0.6%
escape_html long dirty (10KB) 1,080 ± 0.5% 0.9256 200 152.42x slower +1.2%
append long dirty (10KB) 958 ± 3.0% 1.0438 200 171.88x slower +11.4%

render_bench (C ext) · peak RSS 131 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
erubi 7,446 ± 0.8% 0.1343 713 fastest +16.0%
ruby2html 5,739 ± 0.6% 0.1742 419 1.30x slower +4.2%
phlex 555 ± 0.9% 1.8010 6,374 13.41x slower +20.1%

render_bench (pure Ruby) · peak RSS 131 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
erubi 7,489 ± 0.6% 0.1335 713 fastest +10.5%
ruby2html 2,331 ± 0.6% 0.4291 1,029 3.21x slower +3.2%
phlex 568 ± 1.2% 1.7619 6,374 13.19x slower +24.0%

tag_bench (C ext) · peak RSS 39 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
br 215,439 ± 0.6% 0.0046 7.00 fastest -4.6%
li(text) 153,773 ± 0.4% 0.0065 7.02 1.40x slower +1.6%
li(dirty text) 126,175 ± 0.4% 0.0079 7.00 1.71x slower +2.6%
div { block } 98,671 ± 0.3% 0.0101 7.00 2.18x slower -8.5%
span(text, class:) 30,799 ± 0.4% 0.0325 207 6.99x slower +8.4%
div(class:) { block } 29,173 ± 0.4% 0.0343 207 7.38x slower +7.8%
a(text, href:, class:) 23,540 ± 0.4% 0.0425 207 9.15x slower +6.4%

tag_bench (pure Ruby) · peak RSS 26 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
br 80,847 ± 0.4% 0.0124 107 fastest +0.2%
div { block } 62,664 ± 0.5% 0.0160 107 1.29x slower +0.6%
li(text) 53,948 ± 0.4% 0.0185 107 1.50x slower +0.5%
li(dirty text) 41,043 ± 0.3% 0.0244 207 1.97x slower +3.3%
div(class:) { block } 15,521 ± 0.5% 0.0644 207 5.21x slower +4.4%
span(text, class:) 15,437 ± 0.3% 0.0648 207 5.24x slower +5.7%
a(text, href:, class:) 9,623 ± 0.4% 0.1039 207 8.40x slower +2.5%

Rails request (C ext) · peak RSS 235 MB

Benchmark i/s ms / call allocs / call vs fastest Δ vs main
Ruby2html::Component 449 ± 1.1% 2.2265 4,651 fastest +10.9%
ruby2html .html.rb templates 447 ± 1.3% 2.2359 4,795 1.00x slower +5.2%
Slim 443 ± 5.0% 2.2598 4,309 1.01x slower +4.6%
ViewComponent + ruby2html 422 ± 1.4% 2.3705 4,508 1.06x slower +2.4%
ERB 408 ± 11.0% 2.4516 3,064 1.10x slower -0.4%
Phlex 376 ± 1.3% 2.6598 6,089 1.19x slower -4.0%

On a clean runner TruffleRuby has no cached .so, and rake-compiler's
ExtensionTask still runs its copy task expecting tmp/.../ruby2html.so --
which extconf.rb correctly skips building there -- so `rake compile`
failed with ENOENT (it only passed locally thanks to a leftover .so).
Treat TruffleRuby like JRuby: define a no-op compile task; pure Ruby is
faster there anyway.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sebyx07
sebyx07 merged commit ff5ca08 into main Oct 31, 2026
30 checks passed
@sebyx07
sebyx07 deleted the jruby-java-escaper-and-ruby4 branch October 31, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant